package de.lighti.starcraft;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Logger;

import de.lighti.starcraft.ClientEntry.Status;
import de.lighti.starcraft.model.Match;
import de.lighti.starcraft.model.StarcraftBot;
import de.lighti.starcraft.model.StarcraftMap;
import de.lighti.starcraft.network.Message;
import de.lighti.starcraft.network.Message.MESSAGE_TYPE;
import de.lighti.starcraft.util.IniParser;
import de.lighti.starcraft.util.UtilFunctions;

public class Server implements Runnable {

    public enum TOURNAMENT_TYPE {
        randomrobin, roundrobin, benchMark
    }

    private final static Logger LOGGER = Logger.getLogger( Server.class.getName() );
    private final static String SETTINGS_PATH = ".\\..\\Settings\\";
    private final static String TOURNAMENT_INI = SETTINGS_PATH + "Server.ini";
    private final static String BOTS_INI = SETTINGS_PATH + "Bots.ini";

    private final static String MAPS_INI = SETTINGS_PATH + "Maps.ini";
    private static String[] args;

    public static void main( String[] args ) {
        Server.args = args;
        try {
            new Server().run();
        }
        catch (final Exception e) {
            e.printStackTrace();
            LOGGER.severe( e.getLocalizedMessage() );
        }
    }

    private final Vector<StarcraftMap> maps;
    private final Vector<StarcraftBot> bots;
    private int maxRounds;
    private int initialGameID;
    private TOURNAMENT_TYPE tournamentType;
    private int port;

    private final static String STRING_PORT = "port";
    private final static int DEFAULT_PORT = 1337;
    private static final String STRING_ROUNDS = "rounds";
    private static final int DEFAULT_ROUNDS = 1;
    private static final String STRING_INITIAL_GAME_ID = "initialGameID";
    private static final int DEFAULT_INITIAL_GAME_ID = 0;
    private static final String GAMES_LIST = SETTINGS_PATH + "Games.txt";

    private final GameDatabase database;
    private final static String STRING_TOURNAMENT_TYPE = "type";
    private static final TOURNAMENT_TYPE DEFAULT_TOURNAMENT_TYPE = TOURNAMENT_TYPE.benchMark;
    private final Network network;

    public Server() throws IOException {

        network = new Network( this );

        final Properties tournamentProperties = new Properties();
        tournamentProperties.load( new FileReader( new File( TOURNAMENT_INI ) ) );

        final String port = tournamentProperties.getProperty( STRING_PORT );
        setPort( port.trim() );

        final String rounds = tournamentProperties.getProperty( STRING_ROUNDS );
        setRounds( rounds.trim() );

        final String initialGameID = tournamentProperties.getProperty( STRING_INITIAL_GAME_ID );
        setInitialGameID( initialGameID.trim() );

        final String type = tournamentProperties.getProperty( STRING_TOURNAMENT_TYPE );
        setTournamentType( type.trim() );

        maps = IniParser.parseMaps( MAPS_INI );
        bots = IniParser.parseBots( BOTS_INI );

        database = new GameDatabase();
        final boolean renew = args.length > 0 && args[0].equals( "-renew" );
        try {
            final File gamesList = new File( GAMES_LIST );
            if (gamesList.exists() && !renew) {
                LOGGER.info( "Existing " + GAMES_LIST + " found. Trying to rebuild games list from file" );
                database.loadFromFile( this.initialGameID, gamesList, bots, maps );
            }
            else {
                LOGGER.info( "No existing " + GAMES_LIST + " found or -renew specified." );
                database.createNew( tournamentType, bots, maps, maxRounds, this.initialGameID );
                try {
                    gamesList.createNewFile();
                    database.dumpGamesList( gamesList );
                    LOGGER.info( "Written games to " + GAMES_LIST );
                }
                catch (IOException | IllegalArgumentException e) {
                    LOGGER.warning( "Could not write games list to disk: " + e.getLocalizedMessage() );
                }
            }
        }
        catch (final IOException e) {
            LOGGER.severe( e.getLocalizedMessage() );
        }
    }

    private boolean fillBinaryContent( StarcraftBot request ) throws IOException {
        final StarcraftBot s = UtilFunctions.findBotInCollection( bots, request.getName() );
        if (s == null) {
            LOGGER.warning( "Client requested binary data for unknown bot " + request.getName() );
            return false;
        }
        else {
            //If our copy of the map isn't loaded either, make the database load it
            if (s.getBinaryContent() == null) {
                database.loadBotFromFile( s );
            }
            //If it's still not there, we're ignoring the request
            if (s.getBinaryContent() == null) {
                LOGGER.warning( "Unable to fullfill bot request " + s.getName() );
                return false;
            }
            else {
                request.setBinaryContent( s.getBinaryContent() );
            }
            return true;
        }
    }

    private boolean fillBinaryContent( StarcraftMap request ) throws IOException {
        final StarcraftMap s = UtilFunctions.findMapInCollection( maps, request.getName() );
        if (s == null) {
            LOGGER.warning( "Client requested binary data for unknown map " + request.getName() );
            return false;
        }
        else {
            //If our copy of the map isn't loaded either, make the database load it
            if (s.getBinaryContent() == null) {
                database.loadMapFromFile( s );
            }
            //If it's still not there, we're ignoring the request
            if (s.getBinaryContent() == null) {
                LOGGER.warning( "Unable to fullfill map request " + s.getName() );
                return false;
            }
            else {
                request.setBinaryContent( s.getBinaryContent() );
            }
            return true;
        }

    }

    public int getPort() {
        return port;
    }

    /**
     * Network will call this when a new message arrives
     * @param client
     * @param m
     */
    public void handleMessage( ClientEntry client, Message m ) {
        boolean error = false;

        switch (m.getType()) {
            case BUSY:
                client.setStatus( ClientEntry.Status.BUSY );
                updateGamequeue();
                break;
            case READY:
                client.setStatus( ClientEntry.Status.READY );
                updateGamequeue();
                break;
            case REQUEST_BINARY_CONTENT:
                try {
                    final Object payload = m.getPayload();
                    if (payload instanceof StarcraftMap) {
                        //Find the map in our database
                        final StarcraftMap request = (StarcraftMap) payload;
                        if (fillBinaryContent( request )) {
                            //And sent it back to the client
                            network.sendMessage( client, m );
                        }
                        else {
                            error = true;
                        }
                    }
                    else if (payload instanceof StarcraftBot) {
                        final StarcraftBot request = (StarcraftBot) m.getPayload();
                        if (fillBinaryContent( request )) {
                            //And sent it back to the client
                            network.sendMessage( client, m );
                        }
                        else {
                            error = true;
                        }
                    }
                    else {
                        LOGGER.warning( "Client requested binary data for unknown object type " + payload.getClass().getName() + ". Ignoring." );
                        error = true;
                    }
                }
                catch (final IOException e) {
                    LOGGER.severe( "Error while loading map or bot: " + e.getLocalizedMessage() );
                    error = true;
                }
                break;
            case RESULT:
                final Match match = (Match) m.getPayload();
                error = handleResult( match, client );
                break;
            default:
                LOGGER.warning( "Client sent unexpected message " + m.getType() + ". Ignoring." );
                error = true;
                break;

        }
        //In case of an error, say goodbye to the client
        if (error) {
            network.disconnect( client );
        }
    }

    private boolean handleResult( Match match, ClientEntry client ) {
        if (match.getGameID() != client.getCurrentMatch().getGameID()) {
            LOGGER.warning( "Client " + client + " sent a result for a match that wasn't assigned to him. Ignoring" );
            return false;

        }
        final Match serverMatch = database.getMatchInProgess( match.getGameID() );
        if (serverMatch == null) {
            LOGGER.severe( "Received result for a match that is not running but should be." );
        }
        serverMatch.update( match );
        if (serverMatch.canBeDumped()) {
            database.setFinished( serverMatch );
        }
        client.setCurrentMatch( null );
        return true;
    }

    public void rescheduleMatch( Match currentMatch ) {
        LOGGER.info( "Rescheduling match " + currentMatch.getGameID() );
        database.reschedule( currentMatch );

    }

    @Override
    public void run() {
        //At this stage all the tournament related settings have been taken care of by the constructor.
        //We're now starting the network part.

        network.run();
    }

    private void setInitialGameID( String gameid ) {
        if (gameid == null) {
            LOGGER.warning( "No initialGameID specified. Defaulting to  " + DEFAULT_INITIAL_GAME_ID );
            initialGameID = DEFAULT_INITIAL_GAME_ID;
        }
        else {
            try {
                initialGameID = Integer.parseInt( gameid );
                LOGGER.info( "InitialGameID set to " + gameid );
            }
            catch (final NumberFormatException e) {
                LOGGER.warning( "Invalid initialGameID specified. Defaulting to  " + DEFAULT_INITIAL_GAME_ID );
                initialGameID = DEFAULT_INITIAL_GAME_ID;
            }
        }

    }

    private void setPort( String port ) {
        if (port == null) {
            LOGGER.warning( "No port specified. Defaulting to  " + DEFAULT_PORT );
            this.port = DEFAULT_PORT;
        }
        else {
            try {
                this.port = Integer.parseInt( port );
                LOGGER.info( "Port set to " + port );
            }
            catch (final NumberFormatException e) {
                LOGGER.warning( "Invalid port specified. Defaulting to  " + DEFAULT_PORT );
                this.port = DEFAULT_PORT;
            }
        }
    }

    private void setRounds( String rounds ) {
        if (rounds == null) {
            LOGGER.warning( "No rounds specified. Defaulting to  " + DEFAULT_ROUNDS );
            maxRounds = DEFAULT_ROUNDS;
        }
        else {
            try {
                maxRounds = Integer.parseInt( rounds );
                LOGGER.info( "Rounds set to " + rounds );
            }
            catch (final NumberFormatException e) {
                LOGGER.warning( "Invalid rounds specified. Defaulting to  " + DEFAULT_ROUNDS );
                maxRounds = DEFAULT_ROUNDS;
            }
        }

    }

    private void setTournamentType( String type ) {
        if (type == null) {
            LOGGER.warning( "No tournament type specified. Defaulting to  " + DEFAULT_TOURNAMENT_TYPE );
            tournamentType = DEFAULT_TOURNAMENT_TYPE;
        }
        else {
            switch (type) {
                case "randomrobin":
                    tournamentType = TOURNAMENT_TYPE.randomrobin;
                    break;
                case "roundrobin":
                    tournamentType = TOURNAMENT_TYPE.roundrobin;
                    break;
                default:
                    LOGGER.warning( "Invalid tournament type specified. Defaulting to  " + DEFAULT_TOURNAMENT_TYPE );
                case "benchMark":
                    tournamentType = TOURNAMENT_TYPE.benchMark;
                    break;
            }
            LOGGER.info( "Tournament type set to " + tournamentType );
        }

    }

    private void shutdown() {
        LOGGER.info( "Shutting down" );
        network.shutdown();

    }

    private void updateGamequeue() {
        final int needed = database.getClientsNeeded();
        LOGGER.info( "Updating game queue: " + needed + "/" + network.getClientsReady() + " (available/needed)" );
        if (needed <= network.getClientsReady()) {
            final Match m = database.getNextMatch();
            if (m != null) {
                LOGGER.info( "Handing out new match: " + m.getHomeBot() + " vs. " + m.getAwayBot() + " on " + m.getMap() );
                final ClientEntry one = network.getFreeClient();
                one.setMatch( m );
                one.sethost( true );

                database.setInProgress( m );

                network.sendMessage( one, new Message( MESSAGE_TYPE.PLAY, new Object[] { m, true } ) );
                one.setStatus( Status.AWAITNG_REPLY );

                if (needed > 1) {
                    final ClientEntry two = network.getFreeClient();
                    two.setMatch( m );

                    network.sendMessage( two, new Message( MESSAGE_TYPE.PLAY, new Object[] { m, false } ) );
                    two.setStatus( Status.AWAITNG_REPLY );
                }
            }
            else {
                LOGGER.info( "No matches left to play" );
                final Message quitMessage = new Message( MESSAGE_TYPE.QUIT );

                int i = 0;
                for (final ClientEntry c : network.getClients()) {
                    network.sendMessage( c, quitMessage );
                    i++;
                }
                LOGGER.info( "Sent quit message to " + i + " clients." );
                shutdown();
            }
        }
    }

}
